/*
 * Copyright (c) 2017-present, CV4J Contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cv4j.core.filters;

import com.cv4j.core.datamodel.ImageProcessor;
import com.cv4j.image.util.TaskUtils;
import com.cv4j.image.util.Tools;

import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;

/**
 * 高斯模糊滤镜
 *
 * @author dev
 * @date 2021/07/22
 */
public class GaussianBlurFilter extends BaseFilter {
    private float[] kernel;
    private double sigma = 2;
    private ExecutorService mExecutor;
    private CompletionService<Void> service;

    /**
     * 高斯模糊滤镜
     */
    public GaussianBlurFilter() {
        kernel = new float[0];
    }

    /**
     * 设置sigma
     *
     * @param sigma sigma
     */
    public void setSigma(double sigma) {
        this.sigma = sigma;
    }

    /**
     * 做的过滤器
     *
     * @param src src
     * @return {@link ImageProcessor}
     */
    @Override
    public Optional<ImageProcessor> doFilter(final ImageProcessor src) {
        final int size = width * height;
        int dims = src.getChannels();
        makeGaussianKernel(sigma, 0.002, Math.min(width, height));

        mExecutor = TaskUtils.newFixedThreadPool("cv4j", dims);
        service = new ExecutorCompletionService<>(mExecutor);

        // save result
        for (int i = 0; i < dims; i++) {
            final int temp = i;
            service.submit(() -> {
                byte[] inPixels = src.toByte(temp);
                byte[] temp1 = new byte[size];
                blur(inPixels, temp1, width, height); // H Gaussian
                blur(temp1, inPixels, height, width); // V Gaussain
                return null;
            });
        }

        for (int i = 0; i < dims; i++) {
            try {
                service.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        mExecutor.shutdown();
        return Optional.ofNullable(src);
    }

    /**
     * 模糊
     * <p> here is 1D Gaussian        , </p>
     *
     * @param inPixels 以像素为单位
     * @param outPixels 从像素
     * @param width 宽度
     * @param height 高度
     */
    private void blur(byte[] inPixels, byte[] outPixels, int width, int height) {
        int subCol = 0;
        int index = 0;
        int index2 = 0;
        float sum = 0;
        int k1 = kernel.length - 1;
        for (int row = 0; row < height; row++) {
            int c1 = 0;
            index = row;
            for (int col = 0; col < width; col++) {
                sum = 0;
                for (int m = -k1; m < kernel.length; m++) {
                    subCol = col + m;
                    if (subCol < 0 || subCol >= width) {
                        subCol = 0;
                    }
                    index2 = row * width + subCol;
                    c1 = inPixels[index2] & 0xff;
                    sum += c1 * kernel[Math.abs(m)];
                }
                outPixels[index] = (byte) Tools.clamp(sum);
                index += height;
            }
        }
    }

    /**
     * 使高斯核
     *
     * @param sigma σ
     * @param accuracy 精度
     * @param maxRadius 最大半径
     */
    public void makeGaussianKernel(final double sigma, final double accuracy, int maxRadius) {
        int kRadius = (int) Math.ceil(sigma * Math.sqrt(-2 * Math.log(accuracy))) + 1;
        int newMaxRadius = maxRadius;
        // too small maxRadius would result in inaccurate sum.
        if (newMaxRadius < 50) {
            newMaxRadius = 50;
        }
        if (kRadius > newMaxRadius) {
            kRadius = newMaxRadius;
        }
        kernel = new float[kRadius];
        // Gaussian function
        for (int i1 = 0; i1 < kRadius; i1++) {
            kernel[i1] = (float) (Math.exp(-0.5 * i1 * i1 / sigma / sigma));
        }
        // sum over all kernel elements for normalization
        double sum;
        if (kRadius < newMaxRadius) {
            sum = kernel[0];
            for (int i1 = 1; i1 < kRadius; i1++) {
                sum += 2 * kernel[i1];
            }
        } else {
            sum = sigma * Math.sqrt(2 * Math.PI);
        }
        for (int i1 = 0; i1 < kRadius; i1++) {
            double v1 = (kernel[i1] / sum);
            kernel[i1] = (float) v1;
        }
    }
}
